/*
* (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.work;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.nuxeo.ecm.core.work.api.Work.State.RUNNING;
import static org.nuxeo.ecm.core.work.api.Work.State.SCHEDULED;
import javax.naming.NamingException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.nuxeo.ecm.core.api.ConcurrentUpdateException;
import org.nuxeo.ecm.core.work.api.WorkManager;
import org.nuxeo.ecm.core.work.api.WorkQueueMetrics;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.test.NXRuntimeTestCase;
import org.nuxeo.runtime.transaction.TransactionHelper;
public class WorkManagerTXTest extends NXRuntimeTestCase {
protected static final String CATEGORY = "SleepWork";
protected static final String QUEUE = "SleepWork";
protected WorkManager service;
void assertMetrics(long scheduled, long running, long completed, long cancelled) {
assertEquals(new WorkQueueMetrics(QUEUE, scheduled, running, completed, cancelled), service.getMetrics(QUEUE));
}
@Override
@Before
public void setUp() throws Exception {
super.setUp();
deployBundle("org.nuxeo.runtime.jtajca");
deployBundle("org.nuxeo.ecm.core.event");
deployContrib("org.nuxeo.ecm.core.event.test", "test-workmanager-config.xml");
fireFrameworkStarted();
service = Framework.getLocalService(WorkManager.class);
assertNotNull(service);
assertMetrics(0, 0, 0, 0);
TransactionHelper.startTransaction();
}
@Override
@After
public void tearDown() throws Exception {
if (TransactionHelper.isTransactionActiveOrMarkedRollback()) {
TransactionHelper.setTransactionRollbackOnly();
TransactionHelper.commitOrRollbackTransaction();
}
super.tearDown();
}
@Test
public void testWorkManagerPostCommit() throws Exception {
int duration = 1000; // 1s
SleepWork work = new SleepWork(duration, false);
service.schedule(work, true);
assertMetrics(0, 0, 0, 0);
TransactionHelper.commitOrRollbackTransaction();
Thread.sleep(duration + 1000);
assertMetrics(0, 0, 1, 0);
}
@Test
public void testWorkManagerRollback() throws Exception {
Assert.assertTrue(TransactionHelper.isTransactionActive());
int duration = 1000; // 1s
SleepWork work = new SleepWork(duration, false);
service.schedule(work, true);
assertMetrics(0, 0, 0, 0);
TransactionHelper.setTransactionRollbackOnly();
TransactionHelper.commitOrRollbackTransaction();
assertMetrics(0, 0, 0, 0);
}
public static class TestWork extends AbstractWork {
private static final long serialVersionUID = 1L;
public int retryCount;
public int throwCount;
public int runs;
public boolean throwDuringXAResourceEnd;
@Override
public String getTitle() {
return getClass().getName();
}
@Override
public int getRetryCount() {
return retryCount;
}
@Override
public void work() {
runs++;
if (--throwCount >= 0) {
if (!throwDuringXAResourceEnd) {
throw new ConcurrentUpdateException("run " + runs);
} else {
XAResource xaRes = new XAResource() {
@Override
public void commit(Xid xid, boolean onePhase) throws XAException {
}
@Override
public void start(Xid xid, int flags) throws XAException {
}
@Override
public void end(Xid xid, int flags) throws XAException {
// similar code to what we have in SessionImpl#end to deal with ConcurrentUpdateException
Exception e = new ConcurrentUpdateException("end run " + runs);
TransactionHelper.noteSuppressedException(e);
TransactionHelper.setTransactionRollbackOnly();
}
@Override
public void forget(Xid xid) throws XAException {
}
@Override
public int getTransactionTimeout() throws XAException {
return 0;
}
@Override
public boolean isSameRM(XAResource xares) throws XAException {
return false;
}
@Override
public int prepare(Xid xid) throws XAException {
return 0;
}
@Override
public Xid[] recover(int flag) throws XAException {
return null;
}
@Override
public void rollback(Xid xid) throws XAException {
}
@Override
public boolean setTransactionTimeout(int seconds) throws XAException {
return true;
}
};
Transaction transaction;
try {
transaction = TransactionHelper.lookupTransactionManager().getTransaction();
transaction.enlistResource(xaRes);
} catch (SystemException | NamingException | RollbackException e) {
throw new RuntimeException(e);
}
}
}
}
}
@Test
public void testWorkRetryAfterExceptionDuringWork() {
doTestWorkRetryAfterException(false, "");
}
@Test
public void testWorkRetryAfterExceptionDuringCommit() {
doTestWorkRetryAfterException(true, "end ");
}
protected void doTestWorkRetryAfterException(boolean throwDuringXAResourceEnd, String messagePrefix) {
TransactionHelper.commitOrRollbackTransaction();
// regular run
TestWork work = new TestWork();
work.retryCount = 0;
work.throwCount = 0;
work.throwDuringXAResourceEnd = throwDuringXAResourceEnd;
work.run();
assertEquals(1, work.runs);
// retry once
work = new TestWork();
work.retryCount = 1;
work.throwCount = 1;
work.throwDuringXAResourceEnd = throwDuringXAResourceEnd;
work.run();
assertEquals(2, work.runs);
// retry twice
work = new TestWork();
work.retryCount = 2;
work.throwCount = 2;
work.throwDuringXAResourceEnd = throwDuringXAResourceEnd;
work.run();
assertEquals(3, work.runs);
// fail immediately
work = new TestWork();
work.retryCount = 0;
work.throwCount = 1;
work.throwDuringXAResourceEnd = throwDuringXAResourceEnd;
try {
work.run();
fail();
} catch (RuntimeException e) {
assertEquals(1, work.runs);
assertTrue(e.getMessage(), e.getMessage().startsWith("Work failed after 0 retries"));
Throwable cause = e.getCause();
assertTrue(String.valueOf(cause), cause instanceof ConcurrentUpdateException);
assertEquals(messagePrefix + "run 1", cause.getMessage());
assertEquals(0, cause.getSuppressed().length);
}
// retry twice and fail, recording suppressed exceptions
work = new TestWork();
work.retryCount = 1;
work.throwCount = 2;
work.throwDuringXAResourceEnd = throwDuringXAResourceEnd;
try {
work.run();
fail();
} catch (RuntimeException e) {
assertEquals(2, work.runs);
assertTrue(e.getMessage(), e.getMessage().startsWith("Work failed after 1 retry"));
Throwable cause = e.getCause();
assertTrue(String.valueOf(cause), cause instanceof ConcurrentUpdateException);
assertEquals(messagePrefix + "run 1", cause.getMessage());
assertEquals(1, cause.getSuppressed().length);
Throwable suppressed0 = cause.getSuppressed()[0];
assertTrue(suppressed0 instanceof ConcurrentUpdateException);
assertEquals(messagePrefix + "run 2", suppressed0.getMessage());
}
// retry 3 times and fail, recording suppressed exceptions
work = new TestWork();
work.retryCount = 2;
work.throwCount = 3;
work.throwDuringXAResourceEnd = throwDuringXAResourceEnd;
try {
work.run();
fail();
} catch (RuntimeException e) {
assertEquals(3, work.runs);
assertTrue(e.getMessage(), e.getMessage().startsWith("Work failed after 2 retries"));
Throwable cause = e.getCause();
assertTrue(String.valueOf(cause), cause instanceof ConcurrentUpdateException);
assertEquals(messagePrefix + "run 1", cause.getMessage());
assertEquals(2, cause.getSuppressed().length);
Throwable suppressed0 = cause.getSuppressed()[0];
assertTrue(suppressed0 instanceof ConcurrentUpdateException);
assertEquals(messagePrefix + "run 2", suppressed0.getMessage());
Throwable suppressed1 = cause.getSuppressed()[1];
assertTrue(suppressed1 instanceof ConcurrentUpdateException);
assertEquals(messagePrefix + "run 3", suppressed1.getMessage());
}
}
}